import fs from 'fs';
import path from 'path';
import FormData from 'form-data';
import { pathOr } from 'ramda';
import Boom from '@hapi/boom';
import Joi from 'joi';
import camelCase from 'lodash/camelCase';
import upperFirst from 'lodash/upperFirst';
import { format as urlFormat } from 'url';
import fetch from 'fermionjs/lib/fetch';
import debugCheck from '@kuiper-helpers/debug-check';
import pkg from '../../../../package.json';
// import b64ToUtf from '../../helpers/b64ToUtf';
import {
	TASK_TYPE_CATEGORY_MAP,
	WARN_LEVEL_MAP,
	TASK_TYPES,
	SECURITY_EVENT_TYPE_MAP,
	TASK_CATEGORY_KEYS,
	VUL_LEVEL_NUM_EN_MAP,
	VUL_LEVEL_NUM_LABEL_MAP,
	WARNING_CATEGORY_KEY_LABEL_MAP,
	CONTENT_IDX_LABEL_MAP,
} from '../../../common/constants';
import { duration, percent, datetime } from '../../../common/formatter';
import calculateScore, { levelLabel } from '../../../common/calculateScore';
import errorCodesBuilder from '../../helpers/errorCodesBuilder';
import aviliableNodeTable from './aviliableNodeTable';
const { name } = pkg;

const errorCodes = errorCodesBuilder();
const templateFilePath = path.join(__dirname, './template.docx');

const ASSET_CHANGE_TYPE_LABEL_MAP = {
	subdomainAdd: '子域名上线',
	subdomainRemove: '子域名下线',
	portAdd: '端口开放',
	portRemove: '端口关闭',
	portService: '端口服务调整',
};

export function warningStatus({ ignoredTime, isResolved, taskType }) {
	if (ignoredTime) return '已忽略';
	if (taskType === 'asset') return isResolved ? '已确认' : '未确认';
	else if (taskType === 'securityEvent') {
		return isResolved ? '已处理' : '待处理';
	}
	return isResolved ? '已修复' : '待修复';
}

const getMonitorPageCount = pathOr(1, [
	'result',
	'addition',
	'statistics',
	'urls',
]);

function aviliableParserBuilder(taskType) {
	return {
		init({ state }) {
			state[`${taskType}AbnormalTaskMap`] = {};
		},
		each({ ret, task, state }) {
			const taskState = (state[`${taskType}AbnormalTaskMap`][
				task.target
			] = state[`${taskType}AbnormalTaskMap`][task.target] || {
				hasAbnormal: false,
				abnormalTaskCount: 0,
				dateRanges: [],
				duration: 0,
				nodeRespAvgs: {}, // { 'NODE_NAME': [{ time: '', respAvg: 111 }] }
				abnormalNodeMap: {},
				taskCount: 0,
			});
			taskState.taskCount += 1;

			const dateRanges = taskState.dateRanges;
			const lastDateRange = dateRanges[dateRanges.length - 1];

			if (task.securityStatus === 'warning') {
				taskState.abnormalTaskCount += 1;
				taskState.hasAbnormal = true;
				if (!lastDateRange || lastDateRange.endTime) {
					dateRanges.push({ startTime: task.endTime, endTime: '' });
				}
			} else if (lastDateRange) {
				lastDateRange.endTime = task.endTime;
				const duration =
					new Date(lastDateRange.endTime || lastDateRange.startTime).getTime() -
					new Date(lastDateRange.startTime).getTime();
				if (taskState.duration < duration) {
					taskState.duration = duration;
				}
			}

			const { respAvg, detail: nodes } = task.result.addition;

			nodes.forEach(x => {
				const abnormalNode = (taskState.abnormalNodeMap[x.area] =
					taskState.abnormalNodeMap[x.area] || []);
				let last = abnormalNode[abnormalNode.length - 1];
				if (!last) {
					if (x.securityStatus !== 'warning') return;
					last = {
						startTime: task.endTime,
						endTime: null,
						ip: x.ip,
						avgTime: x.totalTime,
						duration: 0,
						abnormalCount: 0,
					};
					abnormalNode.push(last);
				}
				last.avgTime = (last.avgTime + x.totalTime) / 2;

				if (x.securityStatus !== 'warning') {
					last.endTime = task.endTime;
					last.duration =
						new Date(last.endTime).getTime() -
						new Date(last.startTime).getTime();
				} else {
					last.abnormalCount += 1;
				}

				const respAvgs = (taskState.nodeRespAvgs[x.area] =
					taskState.nodeRespAvgs[x.area] || []);
				respAvgs.push([task.endTime, respAvg]);
			});
		},
		done({ ret, state }) {
			const result = ret[TASK_TYPE_CATEGORY_MAP[taskType]][taskType];

			const abnormalNodeCounts = {};
			Object.entries(state[`${taskType}AbnormalTaskMap`]).forEach(
				([
					target,
					{
						taskCount,
						abnormalTaskCount,
						hasAbnormal,
						abnormalNodeMap,
						dateRanges,
					},
				]) => {
					result.taskCount = taskCount;
					result.abnormalJobCount += hasAbnormal ? 1 : 0;
					result.abnormalTaskCount += hasAbnormal ? abnormalTaskCount : 0;

					Object.entries(abnormalNodeMap).forEach(([node, dateRanges]) => {
						abnormalNodeCounts[node] = abnormalNodeCounts[node] || 0;
						abnormalNodeCounts[node] += dateRanges.reduce(
							(sum, x) => sum + x.abnormalCount,
							0
						);
						dateRanges.forEach(x => {
							if (x.endTime) return;
							x.endTime = ret.endTime;
							x.duration = ret.endTime.getTime() - x.startTime.getTime();
						});
					});

					dateRanges.forEach(x => {
						if (x.endTime) return;
						x.endTime = ret.endTime;
						x.duration = ret.endTime.getTime() - x.startTime.getTime();
					});
				}
			);
			result.abnormalNodeCount = Object.keys(abnormalNodeCounts).length;

			const {
				mostAbnormalNode,
				mostAbnormalNodeWarningCount,
				abnormalTimesTotal,
			} = Object.entries(abnormalNodeCounts).reduce(
				(obj, [node, count]) => {
					obj.abnormalTimesTotal += count;
					if (obj.mostAbnormalNodeWarningCount > count) return obj;
					return {
						...obj,
						mostAbnormalNode: node,
						mostAbnormalNodeWarningCount: count,
					};
				},
				{
					abnormalTimesTotal: 0,
					mostAbnormalNode: '',
					mostAbnormalNodeWarningCount: 0,
				}
			);
			result.mostAbnormalNode = mostAbnormalNode;
			result.mostAbnormalNodeWarningCount = mostAbnormalNodeWarningCount;
			result.mostAbnormalNodeWarningPercent = percent(
				mostAbnormalNodeWarningCount / result.warningCount
			);

			result.abnormalNodes = Object.entries(abnormalNodeCounts).reduce(
				(arr, [node, times]) => {
					if (times > 0) {
						arr.push({
							node,
							times,
							percent: percent(times / abnormalTimesTotal),
						});
					}
					return arr;
				},
				[]
			);

			result.abnormalJobs = Object.entries(
				state[`${taskType}AbnormalTaskMap`]
			).reduce(
				(
					arr,
					[
						target,
						{
							taskCount,
							abnormalTaskCount,
							hasAbnormal,
							abnormalNodeMap,
							nodeRespAvgs,
						},
					]
				) => {
					if (!hasAbnormal) return arr;

					const job = {
						target,
						taskCount,
						abnormalTaskCount,
						abnormalNodeCount: Object.keys(abnormalNodeMap).length,
					};
					job.abnormalNodes = Object.entries(abnormalNodeMap).map(
						([node, dateRanges]) => {
							return {
								node,
								dateRanges: dateRanges.map(
									({ avgTime, startTime, endTime, ip, duration: d }) => ({
										avgTime,
										ip,
										startTime: datetime(startTime),
										endTime: datetime(endTime),
										duration: duration(d, false),
									})
								),
							};
						}
					);
					job.abnormalNodeTable = aviliableNodeTable(job.abnormalNodes);

					const {
						abnormalLongestNode,
						abnormalLongestDuration,
					} = Object.entries(abnormalNodeMap).reduce(
						(obj, [node, dateRanges]) => {
							dateRanges.forEach(({ duration }) => {
								if (obj.abnormalLongestDuration > duration) return;
								obj.abnormalLongestDuration = duration;
								obj.abnormalLongestNode = node;
							});
							return obj;
						},
						{
							abnormalLongestNode: '',
							abnormalLongestDuration: -1,
						}
					);
					if (result.abnormalLongestDuration < abnormalLongestDuration) {
						result.abnormalLongestNode = abnormalLongestNode;
						result.abnormalLongestDuration = abnormalLongestDuration;
					}

					job.abnormalLongestNode = abnormalLongestNode;
					job.abnormalLongestDuration = duration(abnormalLongestDuration, true);

					job.responseTimeChart = {
						option: {
							title: {
								text: '平均响应时间',
							},
							grid: {
								containLabel: true,
							},
							xAxis: {
								type: 'time',
								splitLine: {
									show: false,
								},
							},
							yAxis: {
								type: 'value',
								boundaryGap: [0, '100%'],
								splitLine: {
									show: false,
								},
							},
							series: Object.entries(nodeRespAvgs).map(([name, data]) => ({
								name,
								data,
								type: 'line',
								showSymbol: false,
							})),
						},
					};

					arr.push(job);

					return arr;
				},
				[]
			);
			result.abnormalLongestDuration = duration(result.abnormalLongestDuration);

			result.abnormalNodesChart = {
				option: {
					title: {
						text: '异常节点占比',
					},
					legend: {
						orient: 'horizontal',
						left: 'center',
						bottom: 0,
						data: result.abnormalNodes.map(x => x.noed),
					},
					series: [
						{
							name: '节点',
							type: 'pie',
							radius: ['50%', '70%'],
							avoidLabelOverlap: false,
							label: {
								formatter: '{b}\n{d}%',
							},
							data: result.abnormalNodes.map(
								({ node: name, times: value }) => ({
									name,
									value,
								})
							),
						},
					],
				},
			};
		},
	};
}

export const parsers = {
	vul: {
		each({ ret, task }) {
			ret.monitorPageCount += getMonitorPageCount(task);
		},
		done({ ret, state }) {
			/**
			 * type 统计
			 */
			const warningTypeTotal = Object.entries(state.warningTitleMap).reduce(
				(sum, [, { idMap }]) => sum + Object.keys(idMap).length,
				0
			);
			ret.weakness.vul.warningTypeCount = Object.values(
				state.warningTitleMap
			).reduce((sum, x) => sum + Object.keys(x.idMap).length, 0);
			Object.values(state.warningTitleMap).forEach(w => {
				const level = upperFirst(VUL_LEVEL_NUM_EN_MAP[w.levelNum] || 'low');
				ret.weakness.vul[`warning${level}Type`] += 1;
				ret.weakness.vul[`warning${level}Count`] += Object.values(
					w.idMap
				).reduce((total, x) => total + x.times, 0);
			});
			/**
			 * warning category 统计
			 */
			const categoryMap = Object.entries(state.warningIdMap).reduce(
				(obj, [, w]) => {
					const category = w.category;
					obj[category] = obj[category] || {
						title: category,
						count: 0,
						levelMap: {},
						percent: '0%',
						warningTypes: {},
					};
					obj[category].count += 1;

					const level = w.level;
					obj[category].levelMap[level] = obj[category].levelMap[level] || 0;
					obj[category].levelMap[level] += 1;

					obj[category].warningTypes[w.title] =
						obj[category].warningTypes[w.title] || 0;
					obj[category].warningTypes[w.title] += 1;

					return obj;
				},
				{}
			);
			ret.weakness.vul.warningCategoryCount = Object.keys(categoryMap).length;
			const mostCategory = Object.values(categoryMap).reduce(
				(obj, x) => {
					Object.keys(x.levelMap).forEach(level => {
						ret.weakness.vul[
							`warning${upperFirst(
								VUL_LEVEL_NUM_EN_MAP[level] || 'low'
							)}Category`
						] += 1;
					});

					if (x.count > obj.count) {
						obj.count = x.count;
						obj.category = x.title;
					}

					return obj;
				},
				{ category: '', count: 0 }
			);
			ret.weakness.vul.mostCategory = mostCategory.category;
			ret.weakness.vul.mostCategoryCount = mostCategory.count;
			ret.weakness.vul.mostCategoryPercent = percent(
				mostCategory.count / warningTypeTotal
			);

			ret.weakness.vul.vulCategories = Object.values(categoryMap).map(x => ({
				...x,
				level: Object.keys(x.levelMap)
					.map(x => VUL_LEVEL_NUM_LABEL_MAP[x])
					.join('/'),
				percent: percent(x.count / ret.weakness.vul.warningTypeCount),
			}));

			ret.weakness.vul.vulCategoriesChart = {
				option: {
					title: {
						text: 'Web漏洞类型占比',
					},
					legend: {
						orient: 'horizontal',
						left: 'center',
						bottom: 0,
						data: Object.keys(categoryMap),
					},
					series: [
						{
							name: 'Web漏洞类型占比',
							type: 'pie',
							radius: ['50%', '70%'],
							avoidLabelOverlap: false,
							label: {
								formatter: '{b}\n{d}%',
							},
							data: Object.entries(categoryMap).map(([name, x]) => ({
								name,
								value: x.count,
							})),
						},
					],
				},
			};
			Object.entries(state.warningTitleMap).forEach(
				([title, { level, category, idMap }]) => {
					const count = Object.keys(idMap).length;
					const p = percent(count / warningTypeTotal);
					if (ret.weakness.vul.mostTypeCount < count) {
						ret.weakness.vul.mostTypeCount = count;
						ret.weakness.vul.mostType = title;
						ret.weakness.vul.mostTypePercent = p;
					}

					const warnings = Object.values(idMap);
					const times = warnings.reduce((a, b) => a + b.times, 0);
					ret.weakness.vul.vulTypes.push({
						title,
						times,
						count,
						level,
						category,
						percent: p,
					});
					const vulsWarningAffects = warnings.map(warn => {
						const { times, affects, title, detail } = warn;
						let request = JSON.stringify(
							warn.traffics[0]?.request?.headers || {}
						)
							.replace(/^\{|\}$/g, '')
							.replace(/^"/g, '')
							.replace(/,"/g, ',')
							.replace(/":/g, ':')
							.replace(/",/g, '"\n');
						request =
							`${warn.traffics[0]?.request.method} ${warn.traffics[0]?.request.url}\n${warn?.traffics[0]?.response.version}\n` +
							request;
						request = request.replace(/",/g, '"\n');
						// request = `${request}\n\n${b64ToUtf(
						// 	warn.traffics[0]?.request.b64content
						// )}`;
						request.includes('undefined') && (request = '暂无数据');
						//
						const { headers } = warn.traffics[0]?.response || {};
						let response = JSON.stringify(headers || {})
							.replace(/^\{|\}$/g, '')
							.replace(/^"/g, '')
							.replace(/,"/g, ',')
							.replace(/":/g, ':');
						response =
							`${warn.traffics[0]?.response?.version} ${warn.traffics[0]?.response?.status_code}\n` +
							response;
						response = response.replace(/",/g, '"\n');
						// response = `${response}\n\n${b64ToUtf(
						// 	warn.traffics[0]?.response.b64content
						// )}`;
						response.includes('undefined') && (response = '暂无数据');
						return {
							times,
							detail: detail?.detail || title.padEnd(1, ' '),
							point: affects.padEnd(1, ' '),
							status: warningStatus(warn),
							cnvd: warn.cnvd[0] || '暂无数据',
							foundAt: datetime(warnings[0].foundAt) || '暂无数据',
							request,
							response,
							payload: warn.payload || '暂无数据',
						};
					});

					ret.weakness.vul.vuls.push({
						title,
						level,
						times,
						count,
						category,
						impact: warnings[0].impact || '暂无数据',
						description: warnings[0].detailText || '暂无数据',
						recommendation: warnings[0].recommendation || '暂无数据',
						percent: p || '暂无数据',
						affects: vulsWarningAffects || '暂无数据',
						reference: warnings[0].reference[0] || '暂无数据',
					});
				}
			);
			ret.weakness.vul.vulTypes.sort((a, b) => b.times - a.times);
		},
	},
	ssl: {
		done({ ret, state }) {
			Object.values(state.warningIdMap).forEach(w => {
				ret.weakness.ssl[`${w.category}WarningCount`] += 1;

				if (w.level === 1) {
					ret.weakness.ssl[`${w.category}WarningLowCount`] += 1;
				} else if (w.level === 2) {
					ret.weakness.ssl[`${w.category}WarningMediumCount`] += 1;
				} else if (w.level === 3) {
					ret.weakness.ssl[`${w.category}WarningHighCount`] += 1;
				}

				if (w.ignoredTime) {
					ret.weakness.ssl[`${w.category}WarningIgnoredCount`] += 1;
				} else if (w.isResolved) {
					ret.weakness.ssl[`${w.category}WarningResolvedCount`] += 1;
				} else if (!w.isResolved) {
					ret.weakness.ssl[`${w.category}WarningUnresolvedCount`] += 1;
				}

				if (w?.detail?.name === 'SSLHostMismatch') {
					ret.weakness.ssl.certificateWarningHostNoMatchCount += 1;
				}
			});

			const warningTypeTotal = Object.entries(state.warningTitleMap).reduce(
				(sum, [, { idMap }]) => sum + Object.keys(idMap).length,
				0
			);
			Object.entries(state.warningTitleMap).forEach(
				([title, { level, category, idMap }]) => {
					const count = Object.keys(idMap).length;

					ret.weakness.ssl[`${category}WarningTypeCount`] += 1;

					const p = percent(count / warningTypeTotal);

					const warnings = Object.values(idMap);
					const times = warnings.reduce((a, b) => a + b.times, 0);
					ret.weakness.ssl.vulTypes.push({
						title,
						times,
						count,
						level,
						percent: p,
					});

					const vulsWarningAffects = warnings.map(warn => {
						const { detail, times, title } = warn;
						return {
							times,
							detail: detail.detail || title,
							point: ret.target,
							status: warningStatus(warn),
						};
					});
					const warning = warnings[0].detail || {};
					// eslint-disable-next-line prettier/prettier
					const cert_info =
						category === 'protocol'
							? {}
							: {
									issuer: warnings[0]?.cert_info?.issuer || '暂无数据',
									subject: warnings[0]?.cert_info?.subject || '暂无数据',
									not_valid_before:
										datetime(warnings[0]?.cert_info?.not_valid_before * 1000) ||
										'暂无数据',
									not_valid_after:
										datetime(warnings[0]?.cert_info?.not_valid_after * 1000) ||
										'暂无数据',
							  };
					ret.weakness.ssl.vuls[category].push({
						...cert_info,
						title: title.padEnd(1, ' '),
						level,
						impact: warning.impact || '暂无数据',
						description: warning.description || '暂无数据',
						recommendation: warning.recommendation || '暂无数据',
						affects: vulsWarningAffects,
						reference: warnings[0].reference[0] || '暂无数据',
						cnvd: warnings[0].cnvd[0] || '暂无数据',
						foundAt: datetime(warnings[0].foundAt) || '暂无数据',
					});
				}
			);
			ret.weakness.ssl.vulTypes.sort((a, b) => b.times - a.times);
		},
	},
	securityEvent: {
		each({ ret, task }) {
			ret.monitorPageCount += getMonitorPageCount(task);
		},
		done({ state, ret }) {
			const countTotal = Object.values(state.warningTitleMap).reduce(
				(sum, x) => sum + Object.keys(x.idMap).length,
				0
			);
			ret.securityEvent.securityEvent.warningTypeCount = countTotal;
			const timesTotal = Object.values(state.warningIdMap).reduce(
				(sum, x) => sum + x.times,
				0
			);
			Object.entries(state.warningTitleMap).forEach(([title, w]) => {
				const count = Object.keys(w.idMap).length;
				ret.securityEvent.securityEvent[
					`${camelCase(w.category)}Count`
				] += count;

				if (ret.securityEvent.securityEvent.mostTypeCount < count) {
					ret.securityEvent.securityEvent.mostType = title;
					ret.securityEvent.securityEvent.mostTypeCount = count;
					ret.securityEvent.securityEvent.mostTypePercent = percent(
						count / countTotal
					);
				}

				Object.values(w.idMap).forEach(x => {
					if (ret.securityEvent.securityEvent.mostTimesCount < x.times) {
						ret.securityEvent.securityEvent.mostTimes = title;
						ret.securityEvent.securityEvent.mostTimesCount = x.times;
						ret.securityEvent.securityEvent.mostTimesPercent = percent(
							x.times / timesTotal
						);
					}
				});
			});

			const categoryMap = Object.entries(state.warningIdMap).reduce(
				(obj, [, w]) => {
					const category = SECURITY_EVENT_TYPE_MAP[w.category];
					obj[category] = obj[category] || {
						title: category,
						times: 0,
						count: 0,
						percent: '0%',
					};
					obj[category].count += 1;
					obj[category].times += w.times;
					return obj;
				},
				{}
			);
			const vulCategories = (ret.securityEvent.securityEvent.vulCategories = Object.values(
				categoryMap
			).map(x => ({
				...x,
				percent: percent(x.count / countTotal),
			})));
			Object.values(state.warningIdMap).reduce((obj, w) => {
				const category = camelCase(w.category);
				switch (category) {
					case 'blackLinks':
						obj.blackLinks[w.addition?.outURL ? 'outter' : 'inner'].push({
							affects: w.affects,
							outURL: w.addition?.outURL,
							status: warningStatus(w),
							times: w.times,
							details: {
								links: w.detail.links,
								keywords: w.detail.keywords,
							},
						});
						break;
					case 'cryjack':
						obj.cryjack[w.addition?.outURL ? 'outter' : 'inner'].push({
							affects: w.affects,
							outURL: w.addition?.outURL,
							status: warningStatus(w),
							times: w.times,
							details: w.detail.map(x => ({
								point: x.pattern,
								link: x.src,
							})),
						});
						break;
					case 'malscan':
						obj.malscan[w.addition?.outURL ? 'outter' : 'inner'].push({
							affects: w.affects,
							outURL: w.addition?.outURL,
							status: warningStatus(w),
							times: w.times,
							details: w.detail.map(x => ({
								point: x.description,
								category: x.category,
								addition: x.url,
							})),
						});
						break;
					case 'brokenLinks':
						obj.brokenLinks.push({
							affects: w.affects,
							status: warningStatus(w),
							times: w.times,
							details: w.detail.map(point => ({ point })),
						});
						break;
					case 'foreignLinks':
						obj.foreignLinks.push({
							affects: w.affects,
							status: warningStatus(w),
							times: w.times,
							links: Object.keys(w.detail),
							details: w.detail,
						});
						break;
				}
				return obj;
			}, ret.securityEvent.securityEvent.vuls);

			ret.securityEvent.securityEvent.warningCategoriesChart = {
				option: {
					title: {
						text: '安全事件类型个数占比',
					},
					legend: {
						orient: 'horizontal',
						left: 'center',
						bottom: 0,
						data: vulCategories.map(x => x.title),
					},
					series: [
						{
							name: '安全事件类型个数',
							type: 'pie',
							radius: ['50%', '70%'],
							avoidLabelOverlap: false,
							label: {
								formatter: '{b}\n{d}%',
							},
							data: vulCategories.map(({ title: name, count: value }) => ({
								name,
								value,
							})),
						},
					],
				},
			};
		},
	},
	ping: aviliableParserBuilder('ping'),
	http: aviliableParserBuilder('http'),
	asset: {
		init({ state }) {
			state.assetAffectsMap = {};
		},
		each({ state, ret, task }) {
			task.result.warnings.forEach(w => {
				const { category, affects } = w;
				const op = w?.detail?.op || '';
				const key = `${w.category}\x01${w.affects}\x01${op}`;
				state.assetAffectsMap[key] = state.assetAffectsMap[key] || {
					category,
					affects,
					op,
					times: 0,
					status: '',
				};
				w.taskType = 'asset';
				state.assetAffectsMap[key].times += 1;
				state.assetAffectsMap[key].status = warningStatus(w);
			});
		},
		done({ state, ret }) {
			Object.values(state.assetAffectsMap).forEach(
				({ category, op, affects, times, status }) => {
					const type = camelCase(`${category}-${op}`);
					ret.asset.asset[`${type}Count`] += 1;
					ret.asset.asset[`${type}WarningCount`] += times;
					ret.asset.asset[type].push({
						times,
						status,
						title: affects,
					});
				}
			);

			const countTotal = Object.values(state.assetAffectsMap).length;
			let types = Object.values(state.assetAffectsMap).reduce(
				(obj, { category, op }) => {
					obj[camelCase(`${category}-${op}`)] = true;
					return obj;
				},
				{}
			);
			types = Object.keys(types);
			types.forEach(type => {
				ret.asset.asset[`${type}CountPercent`] = percent(
					ret.asset.asset[`${type}Count`] / countTotal
				);
			});

			const timesTotal = Object.values(state.assetAffectsMap).reduce(
				(sum, x) => sum + x.times,
				0
			);
			const most = types.reduce(
				(obj, type) => {
					if (ret.asset.asset[`${type}Count`] > obj.mostTypeCount) {
						obj.mostType = ASSET_CHANGE_TYPE_LABEL_MAP[type];
						obj.mostTypeCount = ret.asset.asset[`${type}Count`];
						obj.mostTypePercent = ret.asset.asset[`${type}CountPercent`];
					}
					if (ret.asset.asset[`${type}WarningCount`] > obj.mostFrequentCount) {
						const times = ret.asset.asset[`${type}WarningCount`];
						obj.mostFrequent = ASSET_CHANGE_TYPE_LABEL_MAP[type];
						obj.mostFrequentCount = times;
						obj.mostFrequentPercent = percent(times / timesTotal);
					}
					return obj;
				},
				{
					mostType: '',
					mostTypeCount: 0,
					mostTypePercent: '0%',
					mostFrequent: '',
					mostFrequentCount: 0,
					mostFrequentPercent: '0%',
				}
			);
			Object.assign(ret.asset.asset, most);

			ret.asset.asset.warningTypeChart = {
				option: {
					title: {
						text: '资产变动类型个数分布',
					},
					legend: {
						orient: 'horizontal',
						left: 'center',
						bottom: 0,
						data: types.map(x => ASSET_CHANGE_TYPE_LABEL_MAP[x]),
					},
					series: [
						{
							name: '资产变动类型',
							type: 'pie',
							radius: ['50%', '70%'],
							avoidLabelOverlap: false,
							label: {
								formatter: '{b}\n{d}%',
							},
							data: types.map(type => ({
								name: ASSET_CHANGE_TYPE_LABEL_MAP[type],
								value: ret.asset.asset[`${type}Count`],
							})),
						},
					],
				},
			};
		},
	},
	content: {
		each({ ret, task }) {
			ret.monitorPageCount += getMonitorPageCount(task);
		},
		done({ state, ret }) {
			const countTotal = Object.values(state.warningTitleMap).reduce(
				(sum, x) => sum + Object.keys(x.idMap).length,
				0
			);
			ret.content.content.warningTypeCount = countTotal;
			Object.entries(state.warningIdMap).forEach(([id, w]) => {
				const category = w.category;
				ret.content.content.urlCount += 1;
				ret.content.content[`${category}Count`] += 1;
				ret.content.content.warningCategoryCount[category].count += 1;
				const detail = ret.content.content.warningCategoryDetail[category];
				detail.push({
					affects: w.affects,
					count: w.times,
					status: warningStatus(w),
					category: Object.entries(w.detail).map(([key, words]) => ({
						label: CONTENT_IDX_LABEL_MAP[category][key],
						detail: words.join('、'),
					})),
				});
			});
			Object.entries(state.warningIdMap).forEach(([id, w]) => {
				const category = w.category;
				ret.content.content.warningCategoryCount[category].percent = percent(
					ret.content.content[`${category}Count`] / ret.content.content.urlCount
				);
			});
			ret.content.content.warningCategoriesChart = {
				option: {
					title: {
						text: '内容合规威胁占比',
					},
					legend: {
						orient: 'horizontal',
						left: 'center',
						bottom: 0,
						data: Object.values(ret.content.content.warningCategoryCount).map(
							x => x.label
						),
					},
					series: [
						{
							name: '内容合规威胁个数',
							type: 'pie',
							radius: ['50%', '70%'],
							avoidLabelOverlap: false,
							label: {
								formatter: '{b}\n{d}%',
							},
							data: Object.values(ret.content.content.warningCategoryCount).map(
								({ label: name, count: value }) => ({
									name,
									value,
								})
							),
						},
					],
				},
			};
		},
	},
};

export async function buildReportData({
	Balance,
	Asset,
	Job,
	Task,
	reportType,
	startTime = new Date(),
	endTime = 0,
	jobId,
}) {
	const ret = {
		startTime,
		endTime,
		reportDate: datetime(new Date(), 'yyyy/MM/dd'),
		host: '',
		target: '',
		note: '',
		taskCount: 0,
		monitorPageCount: 0,
		riskLevel: '',
		weakness: {
			alertCount: 0,
			riskRates: [],
			vul: {
				alertCount: 0,
				// grant任务开通与否
				grant: true,
				taskCount: 0,
				warningCount: 0,
				resolvedWarningCount: 0,
				unresolvedWarningCount: 0,
				ignoredWarningCount: 0,
				warningCategoryCount: 0,
				warningHighCategory: 0,
				warningMediumCategory: 0,
				warningLowCategory: 0,
				warningTypeCount: 0,
				warningHighType: 0,
				warningMediumType: 0,
				warningLowType: 0,
				warningHighCount: 0,
				warningMediumCount: 0,
				warningLowCount: 0,
				// 个数最多类别
				mostCategory: '',
				// 个数最多类别个数
				mostCategoryCount: 0,
				// 个数最多类别占比`
				mostCategoryPercent: '0%',
				/**
				 * 漏洞类别列表
				 *              等级           个数       占比
				 * { title: '', level: '高危', count: 0， percent: '0% }
				 */
				vulCategories: [],
				vulCategoriesChart: {
					option: {
						title: {
							text: 'Web漏洞类型占比',
						},
					},
				},
				// 个数最多类型
				mostType: '',
				// 个数最多类型个数
				mostTypeCount: 0,
				// 个数最多类型占比
				mostTypePercent: '0%',
				/**
				 * 漏洞类型列表
				 *              等级           类别          个数      次数      占比
				 * { title: '', level: '高危', category: '', count: 0，times: 0, percent: '0% }
				 */
				vulTypes: [],
				vulTypeChart: {
					option: {
						title: {
							text: 'Web漏洞类型占比',
						},
					},
				},
				/**
				 * 漏洞列表
				 * {
				 * 		title: '名称',
				 * 		level: '高危',
				 * 		description: '描述',
				 *		recommendation: '修复建议',
				 *		times: 1,
				 *		count: 1,
				 *		percent: '0%',
				 * 		affects: [{
				 * 				point: '风险地址',
				 * 				detail: '详情',
				 *  			times: '发现次数',
				 *				status: '已修复' // 状态
				 * 		}]
				 * }
				 */
				vuls: [],
			},
			ssl: {
				alertCount: 0,
				grant: true,
				taskCount: 0,
				warningCount: 0,
				resolvedWarningCount: 0,
				unresolvedWarningCount: 0,
				ignoredWarningCount: 0,
				protocolWarningCount: 0,
				protocolWarningTypeCount: 0,
				protocolWarningHighCount: 0,
				protocolWarningMediumCount: 0,
				protocolWarningLowCount: 0,
				protocolWarningResolvedCount: 0,
				protocolWarningUnresolvedCount: 0,
				protocolWarningIgnoredCount: 0,
				certificateWarningCount: 0,
				certificateWarningHighCount: 0,
				certificateWarningMediumCount: 0,
				certificateWarningLowCount: 0,
				certificateWarningResolvedCount: 0,
				certificateWarningUnresolvedCount: 0,
				certificateWarningIgnoredCount: 0,
				certificateWarningHostNoMatchCount: 0,
				/**
				 * 漏洞类型列表
				 *              等级           个数      次数      占比
				 * { title: '', level: '高危', count: 0，times: 0, percent: '0% }
				 */
				vulTypes: [],

				vuls: {
					/**
					 *	漏洞列表
					 *	{
					 *			title: '名称',
					 *			level: '高危',
					 *			impact: '影响',
					 *			description: '描述',
					 *			recommendation: '修复建议',
					 *			affects: [{
					 *					point: '风险地址',
					 *					detail: '详情',
					 *					times: '发现次数',
					 *					status: '已修复' // 状态
					 *			}]
					 *	}
					 **/
					protocol: [],
					certificate: [],
				},
			},
		},
		// TODO: 拼错了 availiable
		aviliable: {
			alertCount: 0,
			riskRates: [],
			http: {
				alertCount: 0,
				taskCount: 0,
				warningCount: 0,
				abnormalJobCount: 0,
				abnormalTaskCount: 0,
				abnormalNodeCount: 0,
				mostAbnormalNode: '',
				mostAbnormalNodeWarningCount: 0,
				mostAbnormalNodeWarningPercent: '0%',
				mostAbnormalNodeWarningDuration: 0,
				/**
				 * 异常节点列表
				 *             次数      占比
				 * { node: '', times: 0, percent: '0% }
				 */
				abnormalNodes: [],
				abnormalNodesChart: {
					option: {
						title: {
							text: '异常节点',
						},
					},
				},
				/**
				 *	异常详情
				 *	{
				 *		target: 'URL',
				 *		taskCount: 0, // 监测次数
				 *		abnormalTaskCount: 0, // 异常任务数
				 *		abnormalNodeCount: 0, // 异常节点数
				 *		abnormalLongestNode: 0, // 异常最长节点
				 *		abnormalLongestDuration: 0, // 异常最长时间
				 *		responseTimeChart: {
				 *				option: {
				 *					title: {
				 *						text: '平均响应时间'
				 *					},
				 *				}
				 *		},
				 *		abnormalNodes: [{
				 *			node: '成都电信',
				 *			dateRanges: [
				 *				{
				 *					startTime: '2020-11-12 13:04:02',
				 *					endTime: '2020-11-31 01:02:02',
				 *					ip: '风险地址',
				 *					avgTime: '100ms',
				 *					duration: '1:00:30',
				 *				}
				 *			],
				 *		}],
				 *	}
				 */
				abnormalJobs: [],
			},
			ping: {
				alertCount: 0,
				taskCount: 0,
				warningCount: 0,
				abnormalJobCount: 0,
				abnormalTaskCount: 0,
				abnormalNodeCount: 0,
				mostAbnormalNode: '',
				mostAbnormalNodeWarningCount: 0,
				mostAbnormalNodeWarningPercent: '0%',
				mostAbnormalNodeWarningDuration: 0,
				abnormalLongestNode: '',
				abnormalLongestDuration: 0,
				abnormalNodes: [],
				abnormalNodesChart: {
					option: {
						title: {
							text: '异常节点',
						},
					},
				},
				abnormalJobs: [],
			},
		},
		securityEvent: {
			grant: true,
			alertCount: 0,
			riskRates: [],
			securityEvent: {
				alertCount: 0,
				taskCount: 0,
				warningCount: 0,
				resolvedWarningCount: 0,
				unresolvedWarningCount: 0,
				ignoredWarningCount: 0,
				warningTypeCount: 0,
				blackLinksCount: 0,
				cryjackCount: 0,
				malscanCount: 0,
				brokenLinksCount: 0,
				foreignLinksCount: 0,
				// 个数最多
				mostType: '',
				// 个数最多个数
				mostTypeCount: 0,
				// 个数最多占比
				mostTypePercent: '0%',
				// 发现次数最多类型
				mostTimes: '',
				// 发现次数最多类型次数
				mostTimesCount: 0,
				// 发现次数最多类型占比
				mostTimesPercent: '0%',
				warningCategoriesChart: {
					option: {
						title: {
							text: '安全时间类型个数占比',
						},
					},
				},
				/**
				 * 漏洞类别列表
				 *              次数           个数       占比
				 * { title: '', times: 0, count: 0， percent: '0% }
				 */
				vulCategories: [],
				vuls: {
					blackLinks: {
						/**
						 * {
						 * 		affects: 'URL',
						 * 		source: '外链跳转来源地址'
						 *    status: '已修复' // 状态
						 *    times: 0,
						 * 		details: {
						 * 				links: ['风险地址'],
						 * 				keywords: ['附加信息'],
						 * 		}
						 * }
						 */
						inner: [],
						outter: [],
					},
					/**
					 * {
					 * 		affects: 'URL',
					 *    status: '已修复' // 状态
					 *    times: 0,
					 * 		details: [{
					 * 				point: '关键字',
					 * 				link: '辅助信息',
					 * 		}]
					 * }
					 */
					cryjack: {
						inner: [],
						outter: [],
					},
					/**
					 * {
					 * 		affects: 'URL',
					 *    status: '已修复' // 状态
					 *    times: 0,
					 * 		details: [{
					 * 				point: '特征描述',
					 * 				category: '类型',
					 * 				addition: '辅助信息URL',
					 * 		}]
					 * }
					 */
					malscan: {
						inner: [],
						outter: [],
					},
					/**
					 * {
					 * 		affects: 'URL',
					 *    status: '已修复' // 状态
					 *    times: 0,
					 * 		details: [{
					 * 				point: '特征描述',
					 * 		}]
					 * }
					 */
					brokenLinks: [],
					foreignLinks: [],
				},
			},
		},
		asset: {
			grant: true,
			// 告警次数
			alertCount: 0,
			riskRates: [],
			asset: {
				alertCount: 0,
				// 监测次数
				taskCount: 0,
				warningCount: 0,
				// 确认个数
				resolvedWarningCount: 0,
				// 未确认个数
				unresolvedWarningCount: 0,
				// 子域名上线个数
				subdomainAddCount: 0,
				// 个数占比
				subdomainAddCountPercent: '0%',
				// 次数
				subdomainAddWarningCount: 0,
				/**
				 * 变动详情
				 *                             发现次数   最新状态
				 * { title: 'www.example.com', times: 0, status: '待确认' }
				 **/
				subdomainAdd: [],
				// 子域名下线
				subdomainRemoveCount: 0,
				subdomainRemoveCountPercent: '0%',
				subdomainRemoveWarningCount: 0,
				subdomainRemove: [],
				// 端口开放
				portAddCount: 0,
				portAddCountPercent: '0%',
				portAddWarningCount: 0,
				portAdd: [],
				// 端口关闭
				portRemoveCount: 0,
				portRemoveCountPercent: '0%',
				portRemoveWarningCount: 0,
				portRemove: [],
				// 端口服务调整
				portServiceCount: 0,
				portServiceCountPercent: '0%',
				portServiceWarningCount: 0,
				portService: [],
				// 个数最多类型
				mostType: '',
				// 个数最多类型个数
				mostTypeCount: 0,
				// 个数最多类型占比
				mostTypePercent: '0%',
				// 次数最多类型
				mostFrequent: '',
				// 次数最多类型个数
				mostFrequentCount: 0,
				// 次数最多类型占比
				mostFrequentPercent: '0%',
				// 变动类型分布饼图
				warningTypeChart: {
					option: {
						title: {
							text: '资产变动类型个数分布',
						},
					},
				},
			},
		},
		content: {
			grant: true,
			alertCount: 0,
			riskRates: [],
			content: {
				alertCount: 0,
				taskCount: 0,
				warningCount: 0,
				resolvedWarningCount: 0,
				unresolvedWarningCount: 0,
				ignoredWarningCount: 0,
				warningTypeCount: 0,
				urlCount: 0,
				keywordCount: 0,
				privacy_disclosureCount: 0,
				categoryKeyLabelMap: WARNING_CATEGORY_KEY_LABEL_MAP.content,
				warningCategoryCount: {
					keyword: { label: '敏感词', count: 0, percent: '0%' },
					privacy_disclosure: { label: '信息泄露', count: 0, percent: '0%' },
				},
				warningCategoryDetail: {
					// [{ count: 0, status: '已处理', category: [{ label: 'xx', detail: '1、2、3' }] }]
					keyword: [],
					privacy_disclosure: [],
				},
			},
		},
	};
	const states = TASK_TYPES.reduce((obj, type) => {
		obj[type] = {
			warningTitleMap: {},
		};
		return obj;
	}, {});

	Object.entries(parsers).forEach(([taskType, { init }]) => {
		states[taskType] = states[taskType] || {};
		init && init({ ret, state: states[taskType] });
	});

	const [job, asset] = await Promise.all([
		Job.findOne({ _id: jobId }).lean(),
		Asset.findOne({ jobId }).lean(),
	]);
	ret.host = `${job.note} (${urlFormat(job.target)})`;
	ret.target = urlFormat(job.target);
	ret.note = job.note;
	const resBalances = await Balance.findOne({ uid: job.uid });
	ret.weakness.vul.grant =
		resBalances.feature?.vulTask || resBalances.feature?.vulManualTask;
	ret.weakness.ssl.grant =
		resBalances.feature?.sslTask || resBalances.feature?.sslManualTask;
	ret.securityEvent.grant =
		resBalances.feature?.securityEventTask ||
		resBalances.feature?.securityEventManualTask;
	ret.asset.grant =
		resBalances.feature?.assetTask || resBalances.feature?.assetTask;
	let cursor = Task.find({
		jobId,
		endTime: { $gte: startTime, $lte: endTime },
		status: 'completed',
		taskType: { $ne: 'sitePortrait' },
	});

	if (reportType === 'previous') {
		cursor = Task.find({
			status: 'completed',
			_id: {
				$in: TASK_TYPES.reduce((arr, type) => {
					const id = job[`${type}PrevResultId`];
					if (id) arr.push(...[].concat(id));
					return arr;
				}, [].concat(asset.prevResultId || [])),
			},
		});
	} else if (reportType === 'last') {
		cursor = Task.find({
			status: 'completed',
			_id: {
				$in: TASK_TYPES.reduce((arr, type) => {
					const id = job[`${type}ResultId`];
					if (id) arr.push(...[].concat(id));
					return arr;
				}, [].concat(asset?.resultId || [])),
			},
		});
	}

	for await (const t of cursor.sort({ endTime: 1 })) {
		const task = t.toObject ? t.toObject() : t;
		if (reportType !== 'range') {
			const prevStartTime = new Date(ret.startTime || new Date());
			if (prevStartTime.getTime() > new Date(task.endTime).getTime()) {
				ret.startTime = task.endTime;
			}

			const prevEndTime = new Date(ret.endTime || 0);
			if (prevEndTime.getTime() < new Date(task.endTime).getTime()) {
				ret.endTime = task.endTime;
			}
		}

		ret.taskCount += 1;

		const taskType = task.taskType;
		const taskCategory = TASK_TYPE_CATEGORY_MAP[taskType];
		const taskStats = ret[taskCategory] ? ret[taskCategory][taskType] : {};

		if (task.isSendAlert) {
			ret[taskCategory].alertCount += 1;
			ret[taskCategory][taskType].alertCount += 1;
		}

		taskStats.taskCount += 1;

		const state = (states[taskType] = states[taskType] || {});

		/**
		 * 告警数量统计
		 */
		task.result.warnings.forEach(w => {
			ret[taskCategory][taskType].warningCount += 1;

			state.warningTitleMap[w.title] = state.warningTitleMap[w.title] || {
				level: WARN_LEVEL_MAP[w.level],
				levelNum: w.level,
				category: w.category,
				idMap: {},
			};
			state.warningTitleMap[w.title].idMap[w.id] = state.warningTitleMap[
				w.title
			].idMap[w.id] || {
				...w,
				times: 0,
				endTime: task.endTime,
				taskType: task.taskType,
				// 证书详情
				cert_info: task.result?.addition?.cert_info,
			};
			state.warningTitleMap[w.title].idMap[w.id].times += 1;

			if (
				new Date(state.warningTitleMap[w.title].idMap[w.id].endTime).getTime() <
				new Date(task.endTime)
			) {
				state.warningTitleMap[w.title].idMap[w.id].isResolved = w.isResolved;
				state.warningTitleMap[w.title].idMap[w.id].ignoredTime = w.ignoredTime;
			}
		});

		// 评分
		ret[taskCategory].riskRates.push(calculateScore([task.result.warnings]));

		const each = parsers[taskType].each;
		each && each({ ret, state, task });
	}

	Object.entries(states).forEach(([taskType, state]) => {
		state.warningIdMap = Object.values(state.warningTitleMap).reduce(
			(obj, { idMap }) => Object.assign(obj, idMap),
			{}
		);

		Object.values(state.warningIdMap).forEach(w => {
			const category = TASK_TYPE_CATEGORY_MAP[taskType];
			if (w.ignoredTime) {
				ret[category][taskType].ignoredWarningCount += 1;
			} else if (w.isResolved) {
				ret[category][taskType].resolvedWarningCount += 1;
			} else {
				ret[category][taskType].unresolvedWarningCount += 1;
			}
		});
	});

	Object.entries(parsers).forEach(([taskType, { done }]) => {
		states[taskType] = states[taskType] || {};
		done && done({ ret, state: states[taskType] });
	});

	const riskRate = TASK_CATEGORY_KEYS.reduce((sum, categoryKey) => {
		const { riskRates } = ret[categoryKey] || { riskRates: [] };
		if (!riskRates || riskRates.length === 0) return sum;

		const { sum: rate } = riskRates.reverse().reduce(
			({ sum, percent }, rate, idx) => {
				let p = percent;
				if (idx < riskRates.length - 1) {
					p = p / 2;
				}

				return {
					sum: sum + rate * p,
					percent: p,
				};
			},
			{ sum: 0, percent: 1 }
		);

		return sum ? (sum + rate) / 2 : rate;
	}, 0);
	ret.riskLevel = levelLabel(riskRate);
	ret.startTime = datetime(ret.startTime);
	ret.endTime = datetime(ret.endTime);
	ret.blackLinksInner = ret.securityEvent.securityEvent.vuls.blackLinks.inner;
	ret.blackLinksOutter = ret.securityEvent.securityEvent.vuls.blackLinks.outter;
	ret.cryjackInner = ret.securityEvent.securityEvent.vuls.cryjack.inner;
	ret.cryjackOutter = ret.securityEvent.securityEvent.vuls.cryjack.outter;
	ret.malscanInner = ret.securityEvent.securityEvent.vuls.malscan.inner;
	ret.malscanOutter = ret.securityEvent.securityEvent.vuls.malscan.outter;
	ret.alertCount =
		ret.asset.alertCount +
		ret.weakness.alertCount +
		ret.securityEvent.alertCount +
		ret.aviliable.alertCount;
	return ret;
}

export default {
	tags: ['api', 'dashboard', 'report-download'],
	description: '报表报告下载',
	auth: debugCheck('disable-open-api-auth') ? false : { mode: 'required' },

	validate: {
		query: Joi.object({
			reportType: Joi.string()
				.valid('range', 'last', 'previous')
				.default('last')
				.description('报告类型'),
			startTime: Joi.date().description('开始时间'),
			endTime: Joi.date().description('结束时间'),
			jobId: Joi.string().description('资产ID'),
		}).unknown(),
	},

	pre: [
		{
			assign: 'reportData',
			async method(request, h) {
				const {
					query: { jobId, startTime, endTime, reportType },
					server: {
						app: {
							model: { Asset, Task, Job, Balance },
						},
					},
				} = request;

				try {
					const data = await buildReportData({
						Balance,
						jobId,
						startTime,
						endTime,
						reportType,
						Asset,
						Job,
						Task,
					});
					return data;
				} catch (err) {
					const error = errorCodes['[15]报表报告数据生成失败'].from(err);
					request.log.error({ error }, error.rawMessage);
					throw Boom.badImplementation();
				}
			},
		},
	],

	async handler(request, h) {
		// return false;
		const {
			pre: { reportData },
			server: {
				app: {
					config: {
						[name]: { docgenApi },
					},
				},
			},
		} = request;
		let resp;
		try {
			const form = new FormData();
			form.append('template', fs.createReadStream(templateFilePath));
			form.append('data', JSON.stringify(reportData));
			form.append('filename', `[${reportData.note}]ScanV安全监测报告`);
			resp = await fetch(docgenApi, {
				method: 'POST',
				headers: form.getHeaders(),
				body: form,
				responseType: 'raw',
				timeout: 120000,
			});
		} catch (err) {
			const error = errorCodes['[15]报表报告生成失败'].from(err);
			request.log.error({ error, response: err }, error.rawMessage);
			throw Boom.badImplementation('报表报告生成失败', {
				error: error.message,
			});
		}

		const response = h.response(resp.body);
		Array.from(resp.headers.entries()).forEach(([field, value]) => {
			response.header(field, value);
		});

		request.log.audit(
			{
				affects: reportData.host,
				actionType: 'read',
				actionMessage: '报告下载',
				detail: `下载资产名称为${reportData.note}，资产地址为${reportData.host}的监测报告。`,
			},
			`[${reportData.host}]报告下载`
		);

		return response;
	},
};
